package com.element.security.server.service;

import com.alibaba.fastjson.JSON;
import com.common.core.constant.SecurityHeader;
import com.common.core.exception.ExceptionFactory;
import com.common.core.exception.code.SecurityErrorCode;
import com.common.core.results.SingleResult;
import com.element.security.constant.AuthGrantTypeEnum;
import com.element.security.constant.SecurityParams;
import com.element.security.model.co.TokenRefreshCO;
import com.element.security.model.entity.AuthClientE;
import com.element.security.server.config.PasswordConfig;
import com.element.security.server.global.GlobalOAuth2RequestValidator;
import com.element.security.server.global.code.CodeAuthenticationProvider;
import com.element.security.server.global.code.CodeAuthenticationToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.UnsupportedResponseTypeException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.endpoint.AbstractEndpoint;
import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.util.Map;
import java.util.Set;

/**
 * @auther WuJun.Zhang
 * @date 2022/2/18 下午11:25
 */
public class IAuthCodeTokenService extends AbstractEndpoint {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private AuthorizationCodeServices authorizationCodeServices;

    private AuthorizationServerTokenServices authorizationServerTokenServices;

    private ClientDetailsService clientDetailsService;

    private RestTemplate restTemplate;

    private PasswordConfig passwordConfig;

    private final OAuth2RequestValidator oauth2RequestValidator = new GlobalOAuth2RequestValidator();
    private final RedirectResolver redirectResolver = new DefaultRedirectResolver();

    public SingleResult<TokenRefreshCO> code(Map<String, String> parameters) {
        AuthorizationRequest authorizationRequest = this.getOAuth2RequestFactory().createAuthorizationRequest(parameters);
        Set<String> responseTypes = authorizationRequest.getResponseTypes();
        if (!responseTypes.contains(SecurityParams.RESPONSE_TYPE_CODE)
                && !responseTypes.contains(SecurityParams.RESPONSE_TYPE_TOKEN)) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        }
        if (responseTypes.contains(SecurityParams.RESPONSE_TYPE_CODE)) {
            String appKey = parameters.get(SecurityParams.CLIENT_ID);
            if (!StringUtils.hasText(appKey)) {
                throw ExceptionFactory.bizException("app_key is null");
            }
            authorizationRequest.setClientId(appKey);
            if (authorizationRequest.getClientId() == null) {
                throw new InvalidClientException("A client id must be provided");
            }
            String appSecret = parameters.get(SecurityParams.CLIENT_SECRET);
            if (!StringUtils.hasText(appSecret)) {
                throw ExceptionFactory.bizException("app_secret is null");
            }
            CodeAuthenticationToken codeAuthenticationToken = new CodeAuthenticationToken(appKey, appSecret);
            CodeAuthenticationProvider codeAuthenticationProvider = new CodeAuthenticationProvider(clientDetailsService);
            Authentication authentication = codeAuthenticationProvider.authenticate(codeAuthenticationToken);
            AuthClientE client = (AuthClientE) authentication.getPrincipal();
            if (!passwordConfig.getPasswordEncoder().matches(appSecret, client.getClientSecret())) {
                throw new ClientRegistrationException("appSecret is error");
            }
            String redirectUrl = authorizationRequest.getRequestParameters().get(SecurityParams.REDIRECT_URL);
            if (StringUtils.hasText(redirectUrl)) {
                this.redirectResolver.resolveRedirect(redirectUrl, client);
            } else {
                Set<String> urls = client.getRegisteredRedirectUri();
                if (CollectionUtils.isEmpty(urls)) {
                    throw new InvalidRequestException("At least one redirect_uri must be registered with the client.");
                }
                redirectUrl = urls.iterator().next();
            }
            this.oauth2RequestValidator.validateScope(authorizationRequest, client);
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), authentication);
            String code = authorizationCodeServices.createAuthorizationCode(oAuth2Authentication);
            String state = authorizationRequest.getState();
            restTemplate.postForEntity(redirectUriParameter(redirectUrl, code, state), null, Object.class);
            return SingleResult.of();
        }
        if (responseTypes.contains(SecurityParams.RESPONSE_TYPE_TOKEN)) {
            String appCode = parameters.get(SecurityParams.APP_CODE);
            if (!StringUtils.hasText(appCode)) {
                throw ExceptionFactory.bizException("app_code is null");
            }
            OAuth2Authentication oAuth2Authentication = authorizationCodeServices.consumeAuthorizationCode(appCode);
            AuthClientE cloudAuthClientDO = (AuthClientE) oAuth2Authentication.getPrincipal();
            if (null == cloudAuthClientDO) {
                throw ExceptionFactory.bizException("cloudAuthClientDO is null");
            }
            String clientCredentialGrantType = AuthGrantTypeEnum.CLIENT_CREDENTIALS.getGrantType();
            Set<String> authorizedGrantTypes = cloudAuthClientDO.getAuthorizedGrantTypes();
            if (CollectionUtils.isEmpty(authorizedGrantTypes)) {
                throw ExceptionFactory.bizException("无权限");
            }
            if (!authorizedGrantTypes.contains(clientCredentialGrantType)) {
                throw ExceptionFactory.bizException("无clientCredentialGrantType权限");
            }
            authorizationRequest = this.getOAuth2RequestFactory().createAuthorizationRequest(oAuth2Authentication.getOAuth2Request().getRequestParameters());
            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(authorizationRequest, clientCredentialGrantType);
            tokenRequest.setClientId(cloudAuthClientDO.getClientId());
            AppCodeTokenGranter appCodeTokenGranter = new AppCodeTokenGranter(authorizationServerTokenServices,
                    getClientDetailsService(), getOAuth2RequestFactory(), clientCredentialGrantType);
            appCodeTokenGranter.setAllowRefresh(true);
            OAuth2AccessToken oAuth2AccessToken = appCodeTokenGranter.grant(clientCredentialGrantType, tokenRequest);
            TokenRefreshCO token = new TokenRefreshCO();
            token.setAccessToken(oAuth2AccessToken.getValue());
            token.setExpiresIn(oAuth2AccessToken.getExpiresIn());
            OAuth2RefreshToken refreshToken = oAuth2AccessToken.getRefreshToken();
            if (null != refreshToken) {
                token.setRefreshToken(refreshToken.getValue());
            }
            token.setTokenHeader(SecurityHeader.AUTH_SECURITY_HEADERS);
            token.setTokenType(SecurityHeader.AUTH_SECURITY_VALUE);
            return SingleResult.of(token);
        }
        logger.error("无操作:{}", JSON.toJSONString(parameters));
        return SingleResult.failed(SecurityErrorCode.SYS_EXCEPTION_CODE);
    }

    private String redirectUriParameter(String redirectUrl, String code, String state) {
        if (!StringUtils.hasText(code)) {
            throw ExceptionFactory.bizException("code is null");
        }
        redirectUrl = redirectUrl + "?" + SecurityParams.APP_CODE + "=" + code;
        if (StringUtils.hasText(state)) {
            redirectUrl = redirectUrl + "&" + SecurityParams.STATE + "=" + state;
        }
        return redirectUrl;
    }

    public void setAuthorizationCodeServices(AuthorizationCodeServices authorizationCodeServices) {
        this.authorizationCodeServices = authorizationCodeServices;
    }

    public void setAuthorizationServerTokenServices(AuthorizationServerTokenServices authorizationServerTokenServices) {
        this.authorizationServerTokenServices = authorizationServerTokenServices;
    }

    public void setAuthClientDetailsService(ClientDetailsService clientDetailsService) {
        this.clientDetailsService = clientDetailsService;
        this.setClientDetailsService(clientDetailsService);
    }

    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setPasswordConfig(PasswordConfig passwordConfig) {
        this.passwordConfig = passwordConfig;
    }

    private static class AppCodeTokenGranter extends AbstractTokenGranter {

        // private static final String GRANT_TYPE = "client_credentials";

        private boolean allowRefresh;

        protected AppCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
                                      ClientDetailsService clientDetailsService,
                                      OAuth2RequestFactory requestFactory,
                                      String grantType) {
            super(tokenServices, clientDetailsService, requestFactory, grantType);
        }

        public void setAllowRefresh(boolean allowRefresh) {
            this.allowRefresh = allowRefresh;
        }

        public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
            OAuth2AccessToken token = super.grant(grantType, tokenRequest);
            if (token != null) {
                DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(token);
                if (!this.allowRefresh) {
                    defaultOAuth2AccessToken.setRefreshToken(null);
                }

                token = defaultOAuth2AccessToken;
            }

            return token;
        }
    }
}
